hum-cli 0.2.63__py3-none-any.whl → 0.2.64__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.
- hum/__init__.py +1 -1
- hum/client.py +11 -9
- hum/core.py +1 -1
- hum/runtime/config.py +5 -3
- hum/runtime/drivers.py +79 -36
- hum/runtime/llm.py +104 -43
- hum/tui/cli.mjs +5 -5
- {hum_cli-0.2.63.dist-info → hum_cli-0.2.64.dist-info}/METADATA +1 -1
- {hum_cli-0.2.63.dist-info → hum_cli-0.2.64.dist-info}/RECORD +12 -12
- {hum_cli-0.2.63.dist-info → hum_cli-0.2.64.dist-info}/WHEEL +0 -0
- {hum_cli-0.2.63.dist-info → hum_cli-0.2.64.dist-info}/entry_points.txt +0 -0
- {hum_cli-0.2.63.dist-info → hum_cli-0.2.64.dist-info}/licenses/LICENSE +0 -0
hum/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""Hum — an agent harness built as an RL environment that humans can drive."""
|
|
2
|
-
__version__ = "0.2.
|
|
2
|
+
__version__ = "0.2.64"
|
hum/client.py
CHANGED
|
@@ -183,11 +183,14 @@ class VisibleTools(ToolRegistry):
|
|
|
183
183
|
return results
|
|
184
184
|
|
|
185
185
|
|
|
186
|
-
def retry_line(kind: str,
|
|
187
|
-
"""``
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
186
|
+
def retry_line(kind: str, wait_s: float, attempt: int, of: int) -> str:
|
|
187
|
+
"""``Retrying in 30s · attempt 2/10 · capacity``, as Claude Code says it."""
|
|
188
|
+
return f"Retrying in {wait_s:.0f}s · attempt {attempt}/{of} · {kind}"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def waiting_line(s: float) -> str:
|
|
192
|
+
"""An attempt that has shown nothing for ``s`` seconds."""
|
|
193
|
+
return f"still waiting for the model ({s:.0f}s)"
|
|
191
194
|
|
|
192
195
|
|
|
193
196
|
class VisiblePolicy(PolicyDriver):
|
|
@@ -198,14 +201,13 @@ class VisiblePolicy(PolicyDriver):
|
|
|
198
201
|
**({"retries": retries} if retries is not None else {}))
|
|
199
202
|
self.ui = ui
|
|
200
203
|
self.on_retry = self._retrying
|
|
204
|
+
self.on_waiting = lambda s: ui.set_phase(waiting_line(s))
|
|
201
205
|
if hasattr(llm, "on_notice"):
|
|
202
206
|
llm.on_notice = ui.warn # our proxy's word on the person's credits, once
|
|
203
207
|
|
|
204
208
|
def _retrying(self, attempt: int, e: BaseException, delay: float) -> None:
|
|
205
|
-
"""The person sees the
|
|
206
|
-
|
|
207
|
-
self.ui.retry(attempt, (k := failure_of(e).kind), delay, retry_line(k, getattr(self.llm, "model", None),
|
|
208
|
-
delay, attempt, since))
|
|
209
|
+
"""The person sees the wait before the next attempt, which attempt of how many, and the failure's kind."""
|
|
210
|
+
self.ui.retry(attempt, (k := failure_of(e).kind), delay, retry_line(k, delay, attempt, self.retries))
|
|
209
211
|
|
|
210
212
|
async def next(self, messages, tools): # type: ignore[override]
|
|
211
213
|
self.ui.turn_started()
|
hum/core.py
CHANGED
|
@@ -673,7 +673,7 @@ class WireUI(UI):
|
|
|
673
673
|
self._text = []
|
|
674
674
|
self._announced.clear()
|
|
675
675
|
self._fold_thought()
|
|
676
|
-
self.set_phase(
|
|
676
|
+
self.set_phase(text)
|
|
677
677
|
self.wire.send("retry", attempt=attempt, kind=kind, wait_s=wait_s, text=text)
|
|
678
678
|
|
|
679
679
|
def error(self, msg: str) -> None:
|
hum/runtime/config.py
CHANGED
|
@@ -65,9 +65,11 @@ class Config:
|
|
|
65
65
|
api_key: str | None = None
|
|
66
66
|
reasoning_effort: str | None = None
|
|
67
67
|
temperature: float | None = None
|
|
68
|
-
idle_timeout_s:
|
|
68
|
+
# [model] idle_timeout_s: once an attempt is answered, the longest silence between stream events (keepalives count)
|
|
69
|
+
# before it is cut and retried; the gateway keeps a long think alive (hum.runtime.llm.STREAM_IDLE_S)
|
|
70
|
+
idle_timeout_s: float = 300.0
|
|
69
71
|
# [model] retries: requests tried again after a throttle, an upstream failure or a dropped connection
|
|
70
|
-
retries: int = 10 # a
|
|
72
|
+
retries: int = 10 # the most retries of a turn; fewer by kind and wait (hum.runtime.drivers)
|
|
71
73
|
mcp: list[MCPConfig] = field(default_factory=list)
|
|
72
74
|
lsp: list[LSPConfig] = field(default_factory=list) # [[lsp]]: language servers, beside the ones found on PATH
|
|
73
75
|
gate: str = "off"
|
|
@@ -293,7 +295,7 @@ def load(path: Path | None = None) -> Config:
|
|
|
293
295
|
# server's default
|
|
294
296
|
reasoning_effort=_env("HUM_REASONING_EFFORT") or m.get("reasoning_effort") or cred.get("reasoning_effort")
|
|
295
297
|
or None,
|
|
296
|
-
temperature=m.get("temperature"), idle_timeout_s=float(m.get("idle_timeout_s",
|
|
298
|
+
temperature=m.get("temperature"), idle_timeout_s=float(m.get("idle_timeout_s", 300.0)),
|
|
297
299
|
retries=int(m.get("retries", 10)),
|
|
298
300
|
mcp=[_server(s) for s in d.get("mcp", []) if s.get("name")],
|
|
299
301
|
lsp=[LSPConfig(name=str(s["name"]), command=str(s["command"]), args=[str(x) for x in s.get("args", [])],
|
hum/runtime/drivers.py
CHANGED
|
@@ -12,23 +12,30 @@ from typing import Any, Awaitable, Callable
|
|
|
12
12
|
|
|
13
13
|
from pydantic import BaseModel
|
|
14
14
|
|
|
15
|
-
from .llm import
|
|
15
|
+
from .llm import LLM, REQUEST_KEY, RETRY_AFTER_MAX_S, Failure, ProviderError, StreamEvents, failure_of
|
|
16
16
|
from .models import Intervention, Turn
|
|
17
17
|
|
|
18
|
-
# the most
|
|
18
|
+
# the most retries a turn makes after its first attempt (Claude Code's 10). Each attempt is bounded on its own (the
|
|
19
|
+
# client's answer and stream-idle clocks, hum.runtime.llm), never the turn, so a slow first token is waited out; the
|
|
20
|
+
# kinds below are retried fewer times
|
|
19
21
|
RETRIES = 10
|
|
20
|
-
# the
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
# a clock that ran out: the gateway already retried inside the attempt, and each retry thinks the reply again from the
|
|
23
|
+
# start, so twice is all the person waits on it (worst case 3 attempts of ~5 min)
|
|
24
|
+
RETRIES_BY_KIND = {"deadline_exceeded": 2, "stream_idle": 2, "upstream_timeout": 2}
|
|
25
|
+
# a throttle (capacity, overloaded, rate_limit, a 429): the gateway waits it out and retries inside each attempt, so
|
|
26
|
+
# every retry here multiplies its sends. No retry starts once the person has waited this long on the throttle (the
|
|
27
|
+
# failed attempts and the waits between them): worst case this plus one last attempt, ~10 min, against ~50 min for 11
|
|
28
|
+
# attempts the gateway holds to its deadline
|
|
29
|
+
THROTTLE_WAIT_MAX_S = 300.0
|
|
30
|
+
THROTTLE_KINDS = frozenset({"capacity", "overloaded", "rate_limit"})
|
|
31
|
+
# full-jitter backoff when the server named no wait: the ceiling doubles from 1 s to Claude Code's 32 s cap
|
|
32
|
+
# (code.claude.com/docs/en/errors)
|
|
33
|
+
RETRY_BASE_S = 1.0
|
|
34
|
+
RETRY_CAP_S = 32.0
|
|
35
|
+
# how long an attempt may show nothing before the person is told it is still waited on, then how often after
|
|
36
|
+
# (Claude Code's banner after 20 s with no data)
|
|
37
|
+
WAITING_AFTER_S = 20.0
|
|
38
|
+
WAITING_EVERY_S = 1.0
|
|
32
39
|
|
|
33
40
|
|
|
34
41
|
def backoff(attempt: int) -> float:
|
|
@@ -37,33 +44,43 @@ def backoff(attempt: int) -> float:
|
|
|
37
44
|
return random.uniform(0.0, min(RETRY_CAP_S, RETRY_BASE_S * 2 ** (attempt - 1)))
|
|
38
45
|
|
|
39
46
|
|
|
40
|
-
def retry_wait(attempt: int, f: Failure
|
|
41
|
-
"""How long to wait before attempt ``attempt``, or None when there is no retry
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if not f.retryable or left_s <= 0:
|
|
47
|
+
def retry_wait(attempt: int, f: Failure) -> float | None:
|
|
48
|
+
"""How long to wait before attempt ``attempt``, or None when there is no retry (the failure is not retryable;
|
|
49
|
+
failure_of already counts a wait past RETRY_AFTER_MAX_S as that): the wait the server named, else the backoff."""
|
|
50
|
+
if not f.retryable:
|
|
45
51
|
return None
|
|
46
|
-
if f.retry_after is not None
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
return f.retry_after if f.retry_after is not None else backoff(attempt)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def throttle(f: Failure) -> bool:
|
|
56
|
+
return f.kind in THROTTLE_KINDS or f.status == 429
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def said(e: BaseException, f: Failure) -> BaseException:
|
|
60
|
+
"""The failure that ends the turn, as the person reads it: a wait asked for and not waited says so."""
|
|
61
|
+
if isinstance(e, ProviderError) and not f.retryable and f.retry_after is not None:
|
|
62
|
+
m, s = divmod(int(f.retry_after), 60)
|
|
63
|
+
return ProviderError(e.status, f"{e.message} — the provider asks to wait {f'{m}m {s:02d}s' if m else f'{s}s'}, "
|
|
64
|
+
f"past {RETRY_AFTER_MAX_S:.0f} s: not retried", type=e.type, code=e.code,
|
|
65
|
+
retry_after=e.retry_after, body=e.body, location=e.location, should_retry=False,
|
|
66
|
+
kind=e.kind)
|
|
67
|
+
return e
|
|
49
68
|
|
|
50
69
|
|
|
51
70
|
class PolicyDriver:
|
|
52
71
|
author = "policy"
|
|
53
72
|
|
|
54
73
|
def __init__(self, llm: LLM, author: str = "policy", on_text: Any = None, events: StreamEvents | None = None,
|
|
55
|
-
retries: int = RETRIES,
|
|
56
|
-
tool_choice: Any = None):
|
|
74
|
+
retries: int = RETRIES, tool_choice: Any = None):
|
|
57
75
|
self.llm = llm
|
|
58
76
|
self.author = author
|
|
59
77
|
self.tool_choice = tool_choice # a caller that requires one named tool (the judge) says so here
|
|
60
78
|
self.events = events or StreamEvents(text=on_text)
|
|
61
79
|
self.retries = retries
|
|
62
|
-
self.budget_s = budget_s
|
|
63
80
|
self.on_retry: Any = None # client hook: (attempt, exception, delay_s) -> None
|
|
81
|
+
# client hook: (seconds) -> None, while an attempt has shown nothing past WAITING_AFTER_S
|
|
82
|
+
self.on_waiting: Callable[[float], None] | None = None
|
|
64
83
|
self.sleep: Callable[[float], Awaitable[Any]] | None = None # the wait between attempts; asyncio.sleep unset
|
|
65
|
-
self.clock: Callable[[], float] = time.monotonic # the budget's clock
|
|
66
|
-
self.started_at: float | None = None # the turn's first send, on ``clock``
|
|
67
84
|
# the last call as the person waited for it: request_at, first_at (first streamed delta), done_at, attempts
|
|
68
85
|
self.timing: dict[str, Any] | None = None
|
|
69
86
|
# the last call as the record keeps it, with the attempts that failed before it; the loop takes it with the node
|
|
@@ -93,17 +110,23 @@ class PolicyDriver:
|
|
|
93
110
|
reasoning=(lambda s: (seen(), ev.reasoning(s))[1]) if ev.reasoning else None,
|
|
94
111
|
tool=(lambda i, n, a: (seen(), ev.tool(i, n, a))[1]) if ev.tool else None)
|
|
95
112
|
attempt = 0
|
|
96
|
-
|
|
113
|
+
by_kind: dict[str, int] = {} # the retries of each kind so far
|
|
114
|
+
throttled = 0.0 # the time the person has waited on a throttle: its failed attempts and the waits after them
|
|
115
|
+
loop = asyncio.get_running_loop()
|
|
97
116
|
key = REQUEST_KEY.set(f"hum-{uuid.uuid4()}") # one per turn: every retry below is the same request again
|
|
98
|
-
budget_key = ATTEMPT_BUDGET_S.set(None)
|
|
99
117
|
try:
|
|
100
118
|
while True:
|
|
101
119
|
# each attempt is its own call: a delta streamed by one that then failed is not this one's first token
|
|
102
120
|
t_req, first[0] = time.monotonic(), None
|
|
103
|
-
|
|
121
|
+
waiting = asyncio.ensure_future(self._waiting(first)) if self.on_waiting else None
|
|
122
|
+
sent = loop.time()
|
|
104
123
|
try:
|
|
105
|
-
|
|
106
|
-
|
|
124
|
+
try:
|
|
125
|
+
t = await self.llm.complete(messages, tools, events=timed,
|
|
126
|
+
**({"tool_choice": self.tool_choice} if self.tool_choice else {}))
|
|
127
|
+
finally:
|
|
128
|
+
if waiting is not None:
|
|
129
|
+
waiting.cancel() # the attempt is over: the wait before the next is said by on_retry
|
|
107
130
|
break
|
|
108
131
|
except asyncio.CancelledError:
|
|
109
132
|
raise
|
|
@@ -113,19 +136,29 @@ class PolicyDriver:
|
|
|
113
136
|
"error": (lc or {}).get("error") or {"type": type(e).__name__, "message": str(e)}})
|
|
114
137
|
if lc is not None:
|
|
115
138
|
self.llm.last_call = None
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
139
|
+
f = failure_of(e)
|
|
140
|
+
if throttle(f):
|
|
141
|
+
throttled += loop.time() - sent
|
|
142
|
+
delay = retry_wait(attempt + 1, f) if attempt < self.retries else None
|
|
143
|
+
if delay is not None and by_kind.get(f.kind, 0) >= RETRIES_BY_KIND.get(f.kind, self.retries):
|
|
144
|
+
delay = None
|
|
145
|
+
if delay is not None and throttle(f):
|
|
146
|
+
if throttled + delay > THROTTLE_WAIT_MAX_S:
|
|
147
|
+
delay = None
|
|
148
|
+
else:
|
|
149
|
+
throttled += delay
|
|
119
150
|
if delay is None:
|
|
120
151
|
self._take_call(failed) # the calls that were made are on the record even when the turn is not
|
|
152
|
+
if (out := said(e, f)) is not e:
|
|
153
|
+
raise out from e
|
|
121
154
|
raise
|
|
122
155
|
attempt += 1
|
|
156
|
+
by_kind[f.kind] = by_kind.get(f.kind, 0) + 1
|
|
123
157
|
if self.on_retry:
|
|
124
158
|
self.on_retry(attempt, e, delay)
|
|
125
159
|
await (self.sleep or asyncio.sleep)(delay)
|
|
126
160
|
finally:
|
|
127
161
|
REQUEST_KEY.reset(key)
|
|
128
|
-
ATTEMPT_BUDGET_S.reset(budget_key)
|
|
129
162
|
t_done = time.monotonic()
|
|
130
163
|
self.timing = {"started_at": t_start, "request_at": t_req,
|
|
131
164
|
"first_at": first[0] if first[0] is not None else t_done, "done_at": t_done,
|
|
@@ -135,6 +168,16 @@ class PolicyDriver:
|
|
|
135
168
|
t = t.model_copy(update={"author": self.author})
|
|
136
169
|
return t
|
|
137
170
|
|
|
171
|
+
async def _waiting(self, first: list[float | None]) -> None:
|
|
172
|
+
"""Past WAITING_AFTER_S of an attempt with no output, the person is told how long it has been, every
|
|
173
|
+
WAITING_EVERY_S until output comes or the attempt ends."""
|
|
174
|
+
loop = asyncio.get_running_loop()
|
|
175
|
+
t0 = loop.time()
|
|
176
|
+
await asyncio.sleep(WAITING_AFTER_S)
|
|
177
|
+
while first[0] is None and self.on_waiting is not None:
|
|
178
|
+
self.on_waiting(loop.time() - t0)
|
|
179
|
+
await asyncio.sleep(WAITING_EVERY_S)
|
|
180
|
+
|
|
138
181
|
def partial(self) -> Turn | None:
|
|
139
182
|
"""What the model had produced when it was interrupted, if the client keeps it."""
|
|
140
183
|
fn = getattr(self.llm, "partial", None)
|
hum/runtime/llm.py
CHANGED
|
@@ -50,11 +50,25 @@ CONTEXT_HEADERS = ("session", "agent", "parent", "branch", "purpose")
|
|
|
50
50
|
# instead of calling the model again. x-client-request-id stays one per attempt: it names the attempt, this the request
|
|
51
51
|
REQUEST_KEY: contextvars.ContextVar[str | None] = contextvars.ContextVar("hum_request_key", default=None)
|
|
52
52
|
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
|
|
53
|
+
# each attempt is bounded on its own, never the turn. Its answer (headers, or the first stream event) is waited 300 s
|
|
54
|
+
# plus 1 s per 32 KB of request body, Claude Code's first-byte bound (code.claude.com/docs/en/network-config), a long
|
|
55
|
+
# prompt being slower to answer. Sent as `x-stainless-timeout` (the Stainless SDKs' declared timeout, seconds), which
|
|
56
|
+
# the gateway narrows its own deadline to, less its 30 s margin, so its holds and waits end first and it answers typed
|
|
57
|
+
FIRST_OUTPUT_S = 300.0
|
|
58
|
+
FIRST_OUTPUT_BYTES_PER_S = 32 * 1024
|
|
59
|
+
# after that, the longest silence between stream events of any kind (a keepalive comment, response.created, a delta)
|
|
60
|
+
# before the stream is taken for dead: Codex's stream_idle_timeout (codex-rs/model-provider-info/src/lib.rs:66), Claude
|
|
61
|
+
# Code's idle timeout. The client's default; [model] idle_timeout_s sets it
|
|
62
|
+
STREAM_IDLE_S = 300.0
|
|
63
|
+
# the longest wait a server may ask for and be waited: past it (an hourly or daily window) a retry is not slept, the
|
|
64
|
+
# person is told (gemini-cli packages/core/src/utils/googleQuotaErrors.ts:120)
|
|
65
|
+
RETRY_AFTER_MAX_S = 300.0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def first_output_s(body_bytes: int) -> float:
|
|
69
|
+
"""How long an attempt whose request is ``body_bytes`` long may go without an answer."""
|
|
70
|
+
return FIRST_OUTPUT_S + body_bytes / FIRST_OUTPUT_BYTES_PER_S
|
|
71
|
+
|
|
58
72
|
|
|
59
73
|
# how the answer travels, not what was asked: left out of the digest
|
|
60
74
|
WIRE_ONLY = ("stream", "stream_options")
|
|
@@ -329,20 +343,22 @@ class ProviderError(Exception):
|
|
|
329
343
|
(the body's ``error.type`` / ``error.code`` in the OpenAI shape, or FastAPI's ``detail`` when it is an
|
|
330
344
|
object); its message; how long it asked us to
|
|
331
345
|
wait; where it sent us instead (the Location header, on a redirect); whether it said to try again (its
|
|
332
|
-
``x-should-retry`` header: True, False, or None when it said nothing);
|
|
333
|
-
|
|
346
|
+
``x-should-retry`` header: True, False, or None when it said nothing); the gateway's catalog kind (the body's
|
|
347
|
+
``nunatak_kind``, beside the wire's native type and code; None from a base that is not the gateway); and the body
|
|
348
|
+
whole. Everything a caller decides on is a field here, never a word read out of a sentence."""
|
|
334
349
|
|
|
335
350
|
def __init__(self, status: int, message: str, *, type: str | None = None, code: str | None = None,
|
|
336
351
|
retry_after: float | None = None, body: str = "", location: str | None = None,
|
|
337
|
-
should_retry: bool | None = None):
|
|
352
|
+
should_retry: bool | None = None, kind: str | None = None):
|
|
338
353
|
self.status, self.message, self.type, self.code, self.retry_after, self.body = (int(status), message, type,
|
|
339
354
|
code, retry_after, body)
|
|
340
|
-
self.location, self.should_retry = location, should_retry
|
|
341
|
-
|
|
355
|
+
self.location, self.should_retry, self.kind = location, should_retry, kind
|
|
356
|
+
named = kind or type
|
|
357
|
+
super().__init__(f"HTTP {self.status}" + (f" {named}" if named else "") + (f": {message}" if message else ""))
|
|
342
358
|
|
|
343
359
|
@classmethod
|
|
344
360
|
def from_response(cls, status: int, body: str, headers: Any = None) -> "ProviderError":
|
|
345
|
-
message, type_, code = body.strip(), None, None
|
|
361
|
+
message, type_, code, kind = body.strip(), None, None, None
|
|
346
362
|
location = (headers.get("location") if headers is not None else None) or None
|
|
347
363
|
try:
|
|
348
364
|
d = json.loads(body)
|
|
@@ -356,6 +372,7 @@ class ProviderError(Exception):
|
|
|
356
372
|
message = str(err.get("message") or message)
|
|
357
373
|
type_ = str(err["type"]) if err.get("type") else None
|
|
358
374
|
code = str(err["code"]) if err.get("code") is not None else None
|
|
375
|
+
kind = str(err["nunatak_kind"]) if err.get("nunatak_kind") else None
|
|
359
376
|
elif isinstance(d.get("detail"), str):
|
|
360
377
|
message = d["detail"]
|
|
361
378
|
elif isinstance(d.get("error"), str):
|
|
@@ -363,7 +380,7 @@ class ProviderError(Exception):
|
|
|
363
380
|
if location and 300 <= status < 400:
|
|
364
381
|
message = f"moved to {location}" # a redirect's body says "Redirecting"; the person needs the address
|
|
365
382
|
return cls(status, message, type=type_, code=code, retry_after=retry_after_of(headers, message), body=body,
|
|
366
|
-
location=location, should_retry=should_retry_of(headers))
|
|
383
|
+
location=location, should_retry=should_retry_of(headers), kind=kind)
|
|
367
384
|
|
|
368
385
|
@classmethod
|
|
369
386
|
def from_frame(cls, err: dict[str, Any]) -> "ProviderError":
|
|
@@ -376,12 +393,19 @@ class ProviderError(Exception):
|
|
|
376
393
|
return cls(status if 400 <= status < 600 else 502, str(err.get("message") or ""),
|
|
377
394
|
type=str(err["type"]) if err.get("type") else None,
|
|
378
395
|
code=str(err["code"]) if err.get("code") is not None else None, body=json.dumps({"error": err}),
|
|
379
|
-
should_retry=r if isinstance(r, bool) else None
|
|
396
|
+
should_retry=r if isinstance(r, bool) else None,
|
|
397
|
+
kind=str(err["nunatak_kind"]) if err.get("nunatak_kind") else None)
|
|
380
398
|
|
|
381
399
|
@property
|
|
382
400
|
def is_quota(self) -> bool:
|
|
383
401
|
"""A 429 that is a budget spent, never a throttle: no retry will help."""
|
|
384
|
-
return (self.type or "") in QUOTA or (self.code or "") in QUOTA
|
|
402
|
+
return (self.type or "") in QUOTA or (self.code or "") in QUOTA or self.is_daily_quota
|
|
403
|
+
|
|
404
|
+
@property
|
|
405
|
+
def is_daily_quota(self) -> bool:
|
|
406
|
+
"""A quota that resets daily: the gateway's ``daily_quota`` kind (metaphi-labs/nunatak#232). No retry within the
|
|
407
|
+
session changes it."""
|
|
408
|
+
return self.kind == "daily_quota"
|
|
385
409
|
|
|
386
410
|
|
|
387
411
|
CREDITS_WARN_AT = 0.8 # the share of credits used at which the person hears it, once per session
|
|
@@ -400,10 +424,24 @@ def error_text(e: BaseException) -> str:
|
|
|
400
424
|
return f"{type(e).__name__}: {first}" if first else type(e).__name__
|
|
401
425
|
|
|
402
426
|
|
|
403
|
-
# the gateway's failure kinds (Nunatak's catalog: error.
|
|
404
|
-
# can be answered differently. A kind named terminal
|
|
405
|
-
|
|
427
|
+
# the gateway's failure kinds (Nunatak's catalog, config/errors.json: error.nunatak_kind beside the wire's native type
|
|
428
|
+
# and code, before output and in-band) and whether another request can be answered differently. A kind named terminal
|
|
429
|
+
# is never retried, whatever its status or header says
|
|
430
|
+
RETRYABLE_KINDS = frozenset({"capacity", "overloaded", "rate_limit", "deadline_exceeded"})
|
|
406
431
|
TERMINAL_KINDS = frozenset({"bad_request", "credits", "untyped"})
|
|
432
|
+
# a base that is not the gateway names no kind: its native names read as the kind the gateway would have said for them
|
|
433
|
+
# (the reverse of Nunatak's clients.<wire>.native), the code before the type (OpenAI's type "requests" says nothing)
|
|
434
|
+
NATIVE_KINDS = {"rate_limit_exceeded": "rate_limit", "rate_limit_error": "rate_limit",
|
|
435
|
+
"overloaded_error": "overloaded", "server_is_overloaded": "overloaded",
|
|
436
|
+
"provider_overloaded": "overloaded", "timeout_error": "upstream_timeout",
|
|
437
|
+
"billing_error": "credits", "insufficient_quota": "credits"}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def kind_of(e: ProviderError) -> str:
|
|
441
|
+
"""The failure's catalog kind: the gateway's ``nunatak_kind``, else what a native code or type stands for, else
|
|
442
|
+
the provider's own type, else ``http_<status>``."""
|
|
443
|
+
return (e.kind or NATIVE_KINDS.get(e.code or "") or NATIVE_KINDS.get(e.type or "") or e.type
|
|
444
|
+
or f"http_{e.status}")
|
|
407
445
|
|
|
408
446
|
|
|
409
447
|
@dataclass(frozen=True)
|
|
@@ -411,7 +449,8 @@ class Failure:
|
|
|
411
449
|
"""A failed model call as the retry decision reads it, typed once (``failure_of``): its kind (the gateway's
|
|
412
450
|
catalog kind; else ``network`` for no answer at all, ``context_overflow``, or the provider's own type, or
|
|
413
451
|
``http_<status>``), whether another request can help, how long the server asked us to wait (None: it named
|
|
414
|
-
no wait
|
|
452
|
+
no wait; on a failure not retried, only a wait past RETRY_AFTER_MAX_S, the reason it is not), and the wire code,
|
|
453
|
+
status and message it came with."""
|
|
415
454
|
kind: str
|
|
416
455
|
retryable: bool
|
|
417
456
|
retry_after: float | None = None
|
|
@@ -425,12 +464,15 @@ def failure_of(e: BaseException) -> Failure:
|
|
|
425
464
|
retries; else ``x-should-retry`` (or a stream frame's ``retryable``); else a retryable kind; else the status
|
|
426
465
|
rules for a provider that types nothing: a throttle (a 429 that is not a budget), an upstream failure (5xx),
|
|
427
466
|
a timeout or conflict. A budget spent, a bad request or token, a redirect, a prompt too big (whatever its
|
|
428
|
-
status: the loop makes the context smaller instead) are not
|
|
467
|
+
status: the loop makes the context smaller instead) are not, nor a retryable failure that asks for a wait past
|
|
468
|
+
RETRY_AFTER_MAX_S. A connection that dropped or never answered
|
|
429
469
|
(an httpx transport error) is ``network`` and retried."""
|
|
430
470
|
if isinstance(e, ProviderError):
|
|
431
|
-
kind = e
|
|
471
|
+
kind = kind_of(e)
|
|
432
472
|
if is_context_overflow(e):
|
|
433
473
|
kind, retryable = "context_overflow", False
|
|
474
|
+
elif e.is_daily_quota:
|
|
475
|
+
kind, retryable = "daily_quota", False
|
|
434
476
|
elif kind in TERMINAL_KINDS or e.is_quota:
|
|
435
477
|
retryable = False
|
|
436
478
|
elif e.should_retry is not None:
|
|
@@ -440,6 +482,8 @@ def failure_of(e: BaseException) -> Failure:
|
|
|
440
482
|
else:
|
|
441
483
|
# a 3xx says this address is not the host any more: the same request to the same place is answered the same
|
|
442
484
|
retryable = e.status in (408, 409, 425) or e.status >= 500
|
|
485
|
+
if retryable and e.retry_after is not None and e.retry_after > RETRY_AFTER_MAX_S:
|
|
486
|
+
return Failure(kind, False, e.retry_after, e.code, e.status, e.message) # a wait not waited: terminal
|
|
443
487
|
return Failure(kind, retryable, e.retry_after if retryable else None, e.code, e.status, e.message)
|
|
444
488
|
if isinstance(e, httpx.TransportError):
|
|
445
489
|
return Failure("network", True, None, None, None, error_text(e))
|
|
@@ -463,7 +507,7 @@ def is_context_overflow(e: BaseException) -> bool:
|
|
|
463
507
|
the context can be made smaller and the turn sent again."""
|
|
464
508
|
if not isinstance(e, ProviderError):
|
|
465
509
|
return False
|
|
466
|
-
if (e.code or "") in OVERFLOW_CODES or (e.type or "") in OVERFLOW_CODES:
|
|
510
|
+
if e.kind == "context_overflow" or (e.code or "") in OVERFLOW_CODES or (e.type or "") in OVERFLOW_CODES:
|
|
467
511
|
return True
|
|
468
512
|
return e.status == 400 and any(s in e.message.lower() for s in OVERFLOW)
|
|
469
513
|
|
|
@@ -492,7 +536,7 @@ class OpenAICompatClient:
|
|
|
492
536
|
|
|
493
537
|
def __init__(self, model: str, *, api_base: str, api_key: str | None = None, temperature: float | None = None,
|
|
494
538
|
reasoning_effort: str | None = None, max_tokens: int | None = None, timeout: float = 3600.0,
|
|
495
|
-
idle_timeout: float =
|
|
539
|
+
idle_timeout: float = STREAM_IDLE_S, author: str = "policy", transport: Any = None,
|
|
496
540
|
keep_requests: Path | None = None, hum: bool = True, gateway: Gateway | None = None):
|
|
497
541
|
# on a gateway the model is the catalog's name, whole; elsewhere ``openai/`` names the wire, not the model
|
|
498
542
|
strip = gateway is None and model.startswith("openai/")
|
|
@@ -506,9 +550,11 @@ class OpenAICompatClient:
|
|
|
506
550
|
self.request_id: str | None = None # the id the current attempt went up under (x-client-request-id)
|
|
507
551
|
self.temperature, self.reasoning_effort, self.max_tokens, self.timeout = (temperature, reasoning_effort,
|
|
508
552
|
max_tokens, timeout)
|
|
509
|
-
#
|
|
553
|
+
# the longest silence between stream events once the attempt is answered: a model thinking for minutes is kept
|
|
554
|
+
# alive by the gateway's keepalives; a dead stream is silent forever
|
|
510
555
|
self.idle_timeout = idle_timeout
|
|
511
|
-
self.
|
|
556
|
+
self._due: asyncio.Timeout | None = None # the attempt's clock: first output, then each stream event (_call)
|
|
557
|
+
self._answered = False # whether this attempt has been answered yet (headers or any stream event)
|
|
512
558
|
self.author, self._transport = author, transport
|
|
513
559
|
# where to keep each request whole when no server of ours does (the person's own key)
|
|
514
560
|
self.keep_requests = keep_requests
|
|
@@ -567,7 +613,7 @@ class OpenAICompatClient:
|
|
|
567
613
|
b["session_id"] = str(session)[:256]
|
|
568
614
|
return b, []
|
|
569
615
|
|
|
570
|
-
def _headers(self, request_id: str, api_key: str | None) -> dict[str, str]:
|
|
616
|
+
def _headers(self, request_id: str, api_key: str | None, first_s: float) -> dict[str, str]:
|
|
571
617
|
# the client names itself here too: our proxy is a route of the deployment, which keeps a client floor
|
|
572
618
|
from hum.auth import headers as auth_headers
|
|
573
619
|
# the id this client minted for the request, as Claude Code and Codex send it: every hop logs it, so the call
|
|
@@ -576,8 +622,7 @@ class OpenAICompatClient:
|
|
|
576
622
|
"x-client-request-id": request_id}
|
|
577
623
|
if key := REQUEST_KEY.get():
|
|
578
624
|
h["idempotency-key"] = key
|
|
579
|
-
|
|
580
|
-
h["x-stainless-timeout"] = str(max(1, int(budget)))
|
|
625
|
+
h["x-stainless-timeout"] = str(int(first_s))
|
|
581
626
|
if api_key:
|
|
582
627
|
h["authorization"] = f"Bearer {api_key}"
|
|
583
628
|
return h
|
|
@@ -624,8 +669,8 @@ class OpenAICompatClient:
|
|
|
624
669
|
if not self.api_base and self.gateway is None:
|
|
625
670
|
raise ProviderError(401, "no model host: `hum login`, or name an OpenAI-compatible base under [model] "
|
|
626
671
|
"api_base in config.toml", type="no_host")
|
|
627
|
-
#
|
|
628
|
-
timeout = httpx.Timeout(self.timeout, connect=30.0, read=
|
|
672
|
+
# no read timeout: an attempt's own clocks (_call) bound the wait for its answer and each silence after it
|
|
673
|
+
timeout = httpx.Timeout(self.timeout, connect=30.0, read=None)
|
|
629
674
|
async with httpx.AsyncClient(timeout=timeout, transport=self._transport) as http:
|
|
630
675
|
if self.gateway is None:
|
|
631
676
|
return await self._call(http, ev, self._body(messages, tools, tool_choice), "/chat/completions")
|
|
@@ -658,21 +703,37 @@ class OpenAICompatClient:
|
|
|
658
703
|
async def _call(self, http: httpx.AsyncClient, ev: StreamEvents, body: dict[str, Any], path: str, *,
|
|
659
704
|
wire: str | None = None, api_base: str | None = None, api_key: str | None = None,
|
|
660
705
|
record: dict[str, Any] | None = None, route: str | None = None) -> Turn:
|
|
661
|
-
"""One attempt, held to its
|
|
662
|
-
stream
|
|
663
|
-
|
|
706
|
+
"""One attempt, held to FIRST_OUTPUT_S (and its request's size) until it is answered (headers, or its first
|
|
707
|
+
stream event), then to ``idle_timeout`` between stream events of any kind, keepalive comments included. One
|
|
708
|
+
not answered in time is cut, typed ``deadline_exceeded``; one silent after, ``stream_idle``."""
|
|
709
|
+
# serialized once, as httpx would: its size sets the bound
|
|
710
|
+
raw = json.dumps(body, ensure_ascii=False, separators=(",", ":"), allow_nan=False).encode()
|
|
711
|
+
first_s = first_output_s(len(raw))
|
|
712
|
+
self._answered = False
|
|
713
|
+
t0 = time.monotonic()
|
|
664
714
|
try:
|
|
665
|
-
async with asyncio.
|
|
666
|
-
return await self._attempt(http, ev, body, path, wire=wire, api_base=api_base,
|
|
667
|
-
record=record, route=route)
|
|
715
|
+
async with asyncio.timeout_at(asyncio.get_running_loop().time() + first_s) as self._due:
|
|
716
|
+
return await self._attempt(http, ev, body, raw, first_s, path, wire=wire, api_base=api_base,
|
|
717
|
+
api_key=api_key, record=record, route=route)
|
|
668
718
|
except TimeoutError:
|
|
669
|
-
if
|
|
719
|
+
if not self._due.expired():
|
|
670
720
|
raise
|
|
671
|
-
e = ProviderError(504, f"no
|
|
672
|
-
|
|
721
|
+
e = (ProviderError(504, f"no stream event for {self.idle_timeout:.0f} s", type="stream_idle")
|
|
722
|
+
if self._answered else
|
|
723
|
+
ProviderError(504, f"no answer within {first_s:.0f} s", type="deadline_exceeded"))
|
|
724
|
+
p = self._partial
|
|
725
|
+
self._made(body, t0, api_base=api_base or self.api_base, wire=record, error=e,
|
|
726
|
+
response=p.response() if p is not None and not p.empty else None)
|
|
673
727
|
raise e from None
|
|
674
728
|
|
|
675
|
-
|
|
729
|
+
def _alive(self) -> None:
|
|
730
|
+
"""The attempt was answered, or its stream sent something: the idle clock starts over from now."""
|
|
731
|
+
self._answered = True
|
|
732
|
+
if self._due is not None:
|
|
733
|
+
self._due.reschedule(asyncio.get_running_loop().time() + self.idle_timeout)
|
|
734
|
+
|
|
735
|
+
async def _attempt(self, http: httpx.AsyncClient, ev: StreamEvents, body: dict[str, Any], raw: bytes,
|
|
736
|
+
first_s: float, path: str, *,
|
|
676
737
|
wire: str | None = None, api_base: str | None = None, api_key: str | None = None,
|
|
677
738
|
record: dict[str, Any] | None = None, route: str | None = None) -> Turn:
|
|
678
739
|
"""One request and its stream read to the end. ``wire`` is None through Hum's proxy or a base (chat
|
|
@@ -685,7 +746,7 @@ class OpenAICompatClient:
|
|
|
685
746
|
self.request_id = str(uuid.uuid4()) # one per attempt: a retry is another request
|
|
686
747
|
native = wire is not None
|
|
687
748
|
p = self._partial = _Partial(self.author, route or self.model)
|
|
688
|
-
async with http.stream("POST", url,
|
|
749
|
+
async with http.stream("POST", url, content=raw, headers=self._headers(self.request_id, key, first_s)) as r:
|
|
689
750
|
def made(**kw: Any) -> None:
|
|
690
751
|
self._made(body, t0, headers=r.headers, api_base=base, wire=record, **kw)
|
|
691
752
|
|
|
@@ -694,11 +755,14 @@ class OpenAICompatClient:
|
|
|
694
755
|
e = ProviderError.from_response(r.status_code, text, r.headers)
|
|
695
756
|
made(error=e)
|
|
696
757
|
raise e
|
|
758
|
+
self._alive() # answered: from here on, the silence between events is what is bounded
|
|
697
759
|
self._credits(r.headers)
|
|
698
760
|
closed = [False] # the provider said the reply ended: [DONE], or a finish reason on a chunk
|
|
699
761
|
|
|
700
762
|
async def data() -> Any:
|
|
701
763
|
async for line in r.aiter_lines():
|
|
764
|
+
if line:
|
|
765
|
+
self._alive() # any event, a keepalive comment or one the reply does not use included
|
|
702
766
|
if not line.startswith("data:"):
|
|
703
767
|
continue
|
|
704
768
|
raw = line[5:].strip()
|
|
@@ -735,18 +799,15 @@ class OpenAICompatClient:
|
|
|
735
799
|
delta = ch.get("delta") or {}
|
|
736
800
|
if delta.get("content"):
|
|
737
801
|
p.content.append(delta["content"])
|
|
738
|
-
self._first_due.reschedule(None) # output: the person is no longer waiting
|
|
739
802
|
if ev.text:
|
|
740
803
|
ev.text(delta["content"])
|
|
741
804
|
rc = delta.get("reasoning_content") or delta.get("reasoning")
|
|
742
805
|
if rc:
|
|
743
806
|
p.reasoning.append(rc)
|
|
744
|
-
self._first_due.reschedule(None) # output: the person is no longer waiting
|
|
745
807
|
if ev.reasoning:
|
|
746
808
|
ev.reasoning(rc)
|
|
747
809
|
for tc in delta.get("tool_calls") or []:
|
|
748
810
|
i, name, args = p.tool_delta(tc)
|
|
749
|
-
self._first_due.reschedule(None) # output: the person is no longer waiting
|
|
750
811
|
if ev.tool:
|
|
751
812
|
ev.tool(i, name, args)
|
|
752
813
|
except ProviderError as e:
|
hum/tui/cli.mjs
CHANGED
|
@@ -89511,7 +89511,7 @@ var init_install = __esm({
|
|
|
89511
89511
|
"src/install.js"() {
|
|
89512
89512
|
init_define_HUM_CORE_SUBS();
|
|
89513
89513
|
win = process.platform === "win32";
|
|
89514
|
-
MINE = true ? "0.2.
|
|
89514
|
+
MINE = true ? "0.2.64" : "";
|
|
89515
89515
|
npmFix = (version = "") => `npm i -g @metaphi-ai/hum@${version || "latest"} --foreground-scripts`;
|
|
89516
89516
|
NPM_FIX = npmFix("latest");
|
|
89517
89517
|
humHome = (env3 = process.env) => env3.HUM_HOME || path.join(os3.homedir(), ".hum");
|
|
@@ -89866,10 +89866,10 @@ function clientEnv() {
|
|
|
89866
89866
|
platform: process.platform,
|
|
89867
89867
|
arch: process.arch,
|
|
89868
89868
|
terminal: process.env.TERM_PROGRAM || (process.env.WT_SESSION ? "Windows Terminal" : process.env.TERM || ""),
|
|
89869
|
-
tui: true ? "0.2.
|
|
89869
|
+
tui: true ? "0.2.64" : ""
|
|
89870
89870
|
};
|
|
89871
89871
|
}
|
|
89872
|
-
function startFailure(failures, { npm = /^(node|bun)(\.exe)?$/i.test(String(process.execPath).split(/[\\/]/).pop()), platform: platform2 = process.platform, version = true ? "0.2.
|
|
89872
|
+
function startFailure(failures, { npm = /^(node|bun)(\.exe)?$/i.test(String(process.execPath).split(/[\\/]/).pop()), platform: platform2 = process.platform, version = true ? "0.2.64" : "" } = {}) {
|
|
89873
89873
|
const refused = failures.find((f) => f.error && f.error.code !== "ENOENT");
|
|
89874
89874
|
const fix = npm ? npmFix(version) : updateCommand({ kind: "native" }, platform2);
|
|
89875
89875
|
if (!refused) {
|
|
@@ -91384,7 +91384,7 @@ var after = (ms, fn) => {
|
|
|
91384
91384
|
|
|
91385
91385
|
// src/app.jsx
|
|
91386
91386
|
var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1);
|
|
91387
|
-
var MINE2 = true ? "0.2.
|
|
91387
|
+
var MINE2 = true ? "0.2.64" : "";
|
|
91388
91388
|
var fmtDur = (s) => s < 60 ? `${Math.max(1, Math.round(s))}s` : `${Math.floor(s / 60)}m ${Math.round(s % 60)}s`;
|
|
91389
91389
|
var plural = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
|
|
91390
91390
|
var shortSession = (name) => (name || "").replace(/\.jsonl$/, "");
|
|
@@ -94070,7 +94070,7 @@ async function sendPending({
|
|
|
94070
94070
|
await init_theme();
|
|
94071
94071
|
var import_jsx_runtime5 = __toESM(require_jsx_runtime(), 1);
|
|
94072
94072
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
94073
|
-
var MINE3 = true ? "0.2.
|
|
94073
|
+
var MINE3 = true ? "0.2.64" : "0.0.0";
|
|
94074
94074
|
var layout = installLayout();
|
|
94075
94075
|
function completeInstall() {
|
|
94076
94076
|
if (layout.kind !== "npm" || !needsInstall(MINE3)) return;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
hum/__init__.py,sha256=
|
|
1
|
+
hum/__init__.py,sha256=eopcky-Iu9T5iush8TghzpzdP88uA-cB9Jcfq74yEFQ,104
|
|
2
2
|
hum/auth.py,sha256=Nur8ysHt1WVk7yj0ZICIDLoTebIY8g6Eix_cPp9r3T0,18821
|
|
3
3
|
hum/catalog.py,sha256=6INNvyhwu69UZV80Ri9pGRc6H2eRzSsqkW08vSfMhAU,5290
|
|
4
4
|
hum/cli.py,sha256=9YgOzBuoww1sOflXPZoZhKinSHadY_wcLj2a8218gNU,89486
|
|
5
|
-
hum/client.py,sha256=
|
|
6
|
-
hum/core.py,sha256=
|
|
5
|
+
hum/client.py,sha256=KDgrgEsLpH23BpnKR0qoCGfqcyul9Zyua6E-Zd9KQxk,79350
|
|
6
|
+
hum/core.py,sha256=ZDjofAn0mANApJxcEs2ZrJS6Nmhi0_YB-_ll7doeKzg,168290
|
|
7
7
|
hum/errors.py,sha256=xnhAU0A-662IYa4kMI0zbtYfcpo24uRRAPY87BYLHs4,4858
|
|
8
8
|
hum/gitcred.py,sha256=vCynyn5ZewYJZa1vx73jQNy7KCdDjx_ppiIWegSH4wM,3698
|
|
9
9
|
hum/jsonui.py,sha256=5zlJn2n99qbz8xvGsfkhSI9Vkb0bBqzUz-vlBG6HD_8,5688
|
|
@@ -36,12 +36,12 @@ hum/runtime/ask.py,sha256=SgpoVuknFnDEIEc2S9u54ueAQOA8hhQZyxsQmmAsiIw,24084
|
|
|
36
36
|
hum/runtime/background.py,sha256=sFvf-m8DQhLfekr65XlumXU0DKHlOGqj9CVfnrR8lTY,14035
|
|
37
37
|
hum/runtime/channel.py,sha256=bZNaFg7yAt9pgtKLcU4Rah9k3ngMYWiRxClZARBMyJo,4378
|
|
38
38
|
hum/runtime/comms.py,sha256=p03TUazJ75VpNpjLEVeDr-aKyR15n9tmAQCXhMPEKcQ,13653
|
|
39
|
-
hum/runtime/config.py,sha256=
|
|
39
|
+
hum/runtime/config.py,sha256=oML9NShkQQHTuVKczggW2MPyhZJykqUY_KaWxpAPvD4,25000
|
|
40
40
|
hum/runtime/constraints.py,sha256=9WW-Y6W4dNIcKDdtVfxbLBCNYBb4SR18jycOCpf5gPs,6404
|
|
41
41
|
hum/runtime/decision.py,sha256=rUx1ibu5BfwmZkfyfOs1_9fRnOrVCZUk7zytLvarPpA,7876
|
|
42
42
|
hum/runtime/dm.py,sha256=79LUVsGa7Z-wgQumGbLRmQoAbnMgC9jBJz483OaZjHs,7929
|
|
43
43
|
hum/runtime/documents.py,sha256=zIHq3uNJfekF0PZcoQCCsaOZm3HXa8-ThQnQ_u-cpRM,13756
|
|
44
|
-
hum/runtime/drivers.py,sha256=
|
|
44
|
+
hum/runtime/drivers.py,sha256=Z-kO1-b19NOzdOcTTVif2dV0436wv8rwg0Sfb7uOjCU,17482
|
|
45
45
|
hum/runtime/evals.py,sha256=KvOYsI7Qzz2hJTD1z1l42ynxyWifdJLNFlAIA84tk9c,8881
|
|
46
46
|
hum/runtime/executor.py,sha256=VOLe-AA0YB6cSaoGE2ThSpFOHE5_QdBIyysUQ-Mc0kU,14684
|
|
47
47
|
hum/runtime/gateway.py,sha256=AmO060cfJZJjkmNr0EOGKDCG3pbyX21wJ_-XqtBX6S4,10155
|
|
@@ -50,7 +50,7 @@ hum/runtime/grader.py,sha256=bwgu9J6j7K4JTOqIACGdt8zm_jXX6O4uk-It0vfVK8s,5249
|
|
|
50
50
|
hum/runtime/hooks.py,sha256=J75DSe5RVT_NDpBY0FRq6gstSDkklrCwmtC9BxRnKMQ,15967
|
|
51
51
|
hum/runtime/images.py,sha256=4rZ7Sqiw-X0I2mayLA1xkNd8ZwREPf8Iq02kJJyFZX4,11594
|
|
52
52
|
hum/runtime/lead.py,sha256=Q34siKxfH-EpwqIEB5iMXJNGbVFTbGV1rUVMS8dEyDQ,31037
|
|
53
|
-
hum/runtime/llm.py,sha256=
|
|
53
|
+
hum/runtime/llm.py,sha256=phc4fLlPOxIOdBOJG2S-9MEHaWtG82bO8A20-ivwp3Y,47446
|
|
54
54
|
hum/runtime/loop.py,sha256=cv22KY6h2VU2nUllQ3_TAX3ZPNUzkdxA_81v5zJtaYY,101142
|
|
55
55
|
hum/runtime/lsp.py,sha256=1-JhG_KvbfNq_FLnVmVy2NrvK7d3D-2cO8ayWkR9ybc,30319
|
|
56
56
|
hum/runtime/memory.py,sha256=Z2u9boBWxjaQOOOwY50wfWoGFPPFnKcvfaqOv-O4vWU,14429
|
|
@@ -118,10 +118,10 @@ hum/skills/artifact-design/references/reports.md,sha256=5MSv7JGYJHobHXfgAclt_xsf
|
|
|
118
118
|
hum/skills/consult-human/SKILL.md,sha256=5rMKwC5C9L4nn-ifTjQGXl9hsYbeqOCHNl0IEog_T-E,6867
|
|
119
119
|
hum/skills/consult-human/references/examples.md,sha256=hAdTOiJHlv27dTraEyPz0BILqx_8gZj0FPO43b2OMvw,3490
|
|
120
120
|
hum/skills/consult-human/references/hypotheses.md,sha256=a3Hj3_zdxZZS7-bpxXGu2AEgaOV5Q31iVc2VHJ2yjxQ,3263
|
|
121
|
-
hum/tui/cli.mjs,sha256=
|
|
121
|
+
hum/tui/cli.mjs,sha256=226j0vquDgYoX3xd0YOAI38TKxYZK0v6jQhqXl_d3Mk,4215247
|
|
122
122
|
humboldt/__init__.py,sha256=EcK3Ou8qg9DLaYCtm5t2EcU-C8xZxP8yxfGBiQkuW-M,236
|
|
123
|
-
hum_cli-0.2.
|
|
124
|
-
hum_cli-0.2.
|
|
125
|
-
hum_cli-0.2.
|
|
126
|
-
hum_cli-0.2.
|
|
127
|
-
hum_cli-0.2.
|
|
123
|
+
hum_cli-0.2.64.dist-info/METADATA,sha256=VJrmZ2VZcQy4PhmO1jbOZhizrkV7Ikp_Id3kNQkYhy0,1638
|
|
124
|
+
hum_cli-0.2.64.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
125
|
+
hum_cli-0.2.64.dist-info/entry_points.txt,sha256=L-JBWR79zdIsMdTN5K_PSI4HeJ4kwmo1Bm5oc3lVi5A,104
|
|
126
|
+
hum_cli-0.2.64.dist-info/licenses/LICENSE,sha256=-_Jjy2-XAujWZirw6S-ORP1kLKeVuuG8piFwhYLtt8E,742
|
|
127
|
+
hum_cli-0.2.64.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|