@miller-tech/uap 1.222.0 → 2.1.4
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 +19 -9
- package/config/exl3-profiles/qwen38-27b-exl3.env +68 -0
- package/config/llama-profiles/qwen38-27b-dflash2.env +6 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/benchmarks/paired/adapter.d.ts +15 -0
- package/dist/benchmarks/paired/adapter.d.ts.map +1 -1
- package/dist/benchmarks/paired/adapter.js +22 -1
- package/dist/benchmarks/paired/adapter.js.map +1 -1
- package/dist/bin/cli.js +20 -24
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/deliver.d.ts +34 -0
- package/dist/cli/deliver.d.ts.map +1 -1
- package/dist/cli/deliver.js +147 -20
- package/dist/cli/deliver.js.map +1 -1
- package/dist/cli/guided-setup.d.ts.map +1 -1
- package/dist/cli/guided-setup.js +15 -0
- package/dist/cli/guided-setup.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +3 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli/plan.js +8 -2
- package/dist/cli/plan.js.map +1 -1
- package/dist/cli/policy.d.ts.map +1 -1
- package/dist/cli/policy.js +104 -2
- package/dist/cli/policy.js.map +1 -1
- package/dist/cli/quality.d.ts +11 -0
- package/dist/cli/quality.d.ts.map +1 -0
- package/dist/cli/quality.js +213 -0
- package/dist/cli/quality.js.map +1 -0
- package/dist/cli/wizard-config.d.ts.map +1 -1
- package/dist/cli/wizard-config.js +19 -9
- package/dist/cli/wizard-config.js.map +1 -1
- package/dist/cli/worktree.d.ts.map +1 -1
- package/dist/cli/worktree.js +8 -1
- package/dist/cli/worktree.js.map +1 -1
- package/dist/dashboard/savings.d.ts +1 -4
- package/dist/dashboard/savings.d.ts.map +1 -1
- package/dist/dashboard/savings.js +2 -52
- package/dist/dashboard/savings.js.map +1 -1
- package/dist/delivery/acceptance-judge.d.ts +10 -0
- package/dist/delivery/acceptance-judge.d.ts.map +1 -1
- package/dist/delivery/acceptance-judge.js +45 -3
- package/dist/delivery/acceptance-judge.js.map +1 -1
- package/dist/delivery/agentic-executor.d.ts +12 -1
- package/dist/delivery/agentic-executor.d.ts.map +1 -1
- package/dist/delivery/agentic-executor.js +25 -5
- package/dist/delivery/agentic-executor.js.map +1 -1
- package/dist/delivery/bash-sweep.d.ts +12 -0
- package/dist/delivery/bash-sweep.d.ts.map +1 -1
- package/dist/delivery/bash-sweep.js +8 -1
- package/dist/delivery/bash-sweep.js.map +1 -1
- package/dist/delivery/convergence-loop.d.ts.map +1 -1
- package/dist/delivery/convergence-loop.js +6 -4
- package/dist/delivery/convergence-loop.js.map +1 -1
- package/dist/delivery/execution-gate.d.ts +1 -1
- package/dist/delivery/execution-gate.d.ts.map +1 -1
- package/dist/delivery/execution-gate.js +248 -5
- package/dist/delivery/execution-gate.js.map +1 -1
- package/dist/delivery/snapshot.d.ts +16 -0
- package/dist/delivery/snapshot.d.ts.map +1 -1
- package/dist/delivery/snapshot.js +113 -2
- package/dist/delivery/snapshot.js.map +1 -1
- package/dist/delivery/user-validation.d.ts.map +1 -1
- package/dist/delivery/user-validation.js +55 -10
- package/dist/delivery/user-validation.js.map +1 -1
- package/dist/delivery/verifier-ladder.d.ts +24 -0
- package/dist/delivery/verifier-ladder.d.ts.map +1 -1
- package/dist/delivery/verifier-ladder.js +119 -2
- package/dist/delivery/verifier-ladder.js.map +1 -1
- package/dist/generators/claude-md.js +17 -4
- package/dist/generators/claude-md.js.map +1 -1
- package/dist/policies/liveness.d.ts +105 -0
- package/dist/policies/liveness.d.ts.map +1 -0
- package/dist/policies/liveness.js +251 -0
- package/dist/policies/liveness.js.map +1 -0
- package/dist/policies/policy-memory.d.ts.map +1 -1
- package/dist/policies/policy-memory.js +3 -4
- package/dist/policies/policy-memory.js.map +1 -1
- package/dist/quality/baseline.d.ts +29 -0
- package/dist/quality/baseline.d.ts.map +1 -0
- package/dist/quality/baseline.js +79 -0
- package/dist/quality/baseline.js.map +1 -0
- package/dist/quality/complexity.d.ts +38 -0
- package/dist/quality/complexity.d.ts.map +1 -0
- package/dist/quality/complexity.js +239 -0
- package/dist/quality/complexity.js.map +1 -0
- package/dist/quality/config.d.ts +40 -0
- package/dist/quality/config.d.ts.map +1 -0
- package/dist/quality/config.js +77 -0
- package/dist/quality/config.js.map +1 -0
- package/dist/quality/crap.d.ts +11 -0
- package/dist/quality/crap.d.ts.map +1 -0
- package/dist/quality/crap.js +54 -0
- package/dist/quality/crap.js.map +1 -0
- package/dist/quality/languages.d.ts +25 -0
- package/dist/quality/languages.d.ts.map +1 -0
- package/dist/quality/languages.js +150 -0
- package/dist/quality/languages.js.map +1 -0
- package/dist/quality/report.d.ts +38 -0
- package/dist/quality/report.d.ts.map +1 -0
- package/dist/quality/report.js +251 -0
- package/dist/quality/report.js.map +1 -0
- package/dist/quality/scanner.d.ts +32 -0
- package/dist/quality/scanner.d.ts.map +1 -0
- package/dist/quality/scanner.js +127 -0
- package/dist/quality/scanner.js.map +1 -0
- package/dist/quality/tools.d.ts +28 -0
- package/dist/quality/tools.d.ts.map +1 -0
- package/dist/quality/tools.js +189 -0
- package/dist/quality/tools.js.map +1 -0
- package/dist/types/config.d.ts +148 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +35 -0
- package/dist/types/config.js.map +1 -1
- package/docs/ATTRIBUTION.md +0 -1
- package/docs/INDEX.md +38 -4
- package/docs/analysis/MARKET_POSITION.md +179 -0
- package/docs/architecture/OVERVIEW.md +82 -18
- package/docs/architecture/PROTOCOL.md +1 -1
- package/docs/benchmarks/PAIRED_FINDINGS.md +106 -0
- package/docs/benchmarks/PAIRED_HARNESS.md +16 -0
- package/docs/benchmarks/README.md +59 -0
- package/docs/design/UAP_REACTOR.md +0 -1
- package/docs/getting-started/INSTALLATION.md +1 -1
- package/docs/guides/AUTOMATIC.md +0 -14
- package/docs/guides/AUTOMATIC_FEATURES.md +1 -9
- package/docs/guides/COORDINATION.md +1 -1
- package/docs/guides/DELIVERY_PIPELINE.md +5 -0
- package/docs/guides/DEPLOY_BATCHING.md +1 -1
- package/docs/guides/DROIDS_AND_SKILLS.md +1 -1
- package/docs/guides/LOCAL_MODELS.md +6 -1
- package/docs/guides/MCP_ROUTER.md +1 -1
- package/docs/guides/MEMORY.md +1 -1
- package/docs/guides/MULTI_MODEL.md +1 -1
- package/docs/guides/ORCHESTRATOR.md +1 -1
- package/docs/guides/POLICIES.md +19 -3
- package/docs/guides/QWEN38_EXL3.md +114 -0
- package/docs/guides/WORKTREE_WORKFLOW.md +45 -1
- package/docs/integrations/MCP_ROUTER.md +1 -4
- package/docs/plans/deliver-hardening-plan-2026-07-13.md +173 -0
- package/docs/plans/deliver-hardening-review-response-2026-07-13.md +180 -0
- package/docs/plans/k3-uap-uplift-validation.md +216 -0
- package/docs/plans/qwen38-uap-retest-plan.md +196 -0
- package/docs/reference/API.md +1 -1
- package/docs/reference/CLI.md +49 -14
- package/docs/reference/COMPARISON.md +248 -0
- package/docs/reference/CONFIGURATION.md +1 -3
- package/docs/reference/DATABASE_SCHEMA.md +1 -1
- package/docs/reference/FEATURES.md +3 -3
- package/docs/reference/PATTERNS.md +1 -1
- package/docs/reference/PLATFORMS.md +1 -1
- package/package.json +2 -2
- package/scripts/maintenance/update-skills.py +0 -22
- package/scripts/maintenance/verify-compliance.sh +3 -3
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
- package/src/policies/enforcers/adr_guard.py +0 -0
- package/src/policies/enforcers/bearer_lockdown.py +0 -0
- package/src/policies/enforcers/branch_freshness.py +0 -0
- package/src/policies/enforcers/design_token_gate.py +0 -0
- package/src/policies/enforcers/enforcement_self_protect.py +70 -0
- package/src/policies/enforcers/expert_review_required.py +22 -0
- package/src/policies/enforcers/iac_plan_destruction_check.py +0 -0
- package/src/policies/enforcers/quality_metrics_gate.py +447 -0
- package/src/policies/enforcers/ship_loop_gate.py +0 -0
- package/src/policies/enforcers/validate_plan_on_change.py +0 -0
- package/src/policies/enforcers/visual_verification.py +0 -0
- package/src/policies/enforcers/workdir_scope.py +0 -0
- package/src/policies/schemas/policies/expert-review-required.md +9 -1
- package/src/policies/schemas/policies/quality-metrics-gate.md +74 -0
- package/templates/CLAUDE.template.md +1 -1
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/templates/hooks/pre-tool-use-bash.sh +50 -3
- package/templates/hooks/pre-tool-use-edit-write.sh +31 -0
- package/templates/hooks/session-start.sh +15 -0
- package/templates/hooks/uap-policy-gate.sh +187 -5
- package/templates/stryker.config.json +14 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +254 -28
- package/tools/agents/tests/run_acceptance.py +0 -0
- package/tools/agents/tests/test_client_disconnect.py +0 -0
- package/tools/agents/tests/test_cycle_break_exploration.py +0 -0
- package/tools/agents/tests/test_deferral_break.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_all_langs.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_exemptions.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_filepath.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_web_and_bash.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_worktree.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_write_complexity.py +0 -0
- package/tools/agents/tests/test_disconnect_watcher.py +0 -0
- package/tools/agents/tests/test_doubling_break.py +0 -0
- package/tools/agents/tests/test_empty_maxtokens_recovery.py +0 -0
- package/tools/agents/tests/test_empty_tool_loop_break.py +0 -0
- package/tools/agents/tests/test_enforcement_self_protect.py +90 -0
- package/tools/agents/tests/test_enforcer_escape_hatches.py +0 -0
- package/tools/agents/tests/test_error_loop_break.py +0 -0
- package/tools/agents/tests/test_expert_review_pr_scope.py +13 -5
- package/tools/agents/tests/test_fastpath_config.py +0 -0
- package/tools/agents/tests/test_finalize_suppression.py +0 -0
- package/tools/agents/tests/test_gate_evidence.py +0 -0
- package/tools/agents/tests/test_gate_failclosed_indirection.py +0 -0
- package/tools/agents/tests/test_gate_integrity.py +0 -0
- package/tools/agents/tests/test_live_deliver_kill.py +0 -0
- package/tools/agents/tests/test_malformed_unclosed_think.py +0 -0
- package/tools/agents/tests/test_mandate_beats_recon.py +0 -0
- package/tools/agents/tests/test_mandate_deliver.py +0 -0
- package/tools/agents/tests/test_non_stream_resp_log.py +0 -0
- package/tools/agents/tests/test_output_token_ceilings.py +0 -0
- package/tools/agents/tests/test_overflow_truncate_count_tokens.py +0 -0
- package/tools/agents/tests/test_path_containment.py +0 -0
- package/tools/agents/tests/test_path_normalizer_hardened.py +0 -0
- package/tools/agents/tests/test_repeat_call_guard.py +0 -0
- package/tools/agents/tests/test_repeat_guard.py +0 -0
- package/tools/agents/tests/test_session_admission.py +0 -0
- package/tools/agents/tests/test_stream_heartbeat.py +0 -0
- package/tools/agents/tests/test_stream_thinking_deltas.py +405 -0
- package/tools/agents/tests/test_stuck_break_hard.py +0 -0
- package/tools/agents/tests/test_stuck_break_reattach.py +0 -0
- package/tools/agents/tests/test_turn_count_breaker_periodic.py +0 -0
- package/tools/agents/tests/test_upstream_chokepoint.py +0 -0
- package/tools/agents/tests/test_upstream_model_aliases.py +0 -0
- package/tools/agents/tests/test_validate_plan_gate.py +0 -0
- package/tools/agents/tests/test_vision_passthrough.py +0 -0
- package/tools/agents/tests/test_workdir_scope_enforcer.py +0 -0
- package/dist/cli/rtk.d.ts +0 -34
- package/dist/cli/rtk.d.ts.map +0 -1
- package/dist/cli/rtk.js +0 -401
- package/dist/cli/rtk.js.map +0 -1
- package/docs/integrations/RTK.md +0 -113
- package/scripts/setup/install-rtk.sh +0 -184
- package/src/policies/enforcers/rtk_wrap.py +0 -205
- package/src/policies/schemas/policies/rtk-wrap.md +0 -50
- package/tools/agents/tests/test_rtk_wrap.py +0 -151
|
File without changes
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PROXY_STREAM_THINKING_DELTAS: stream reasoning_content as thinking blocks.
|
|
3
|
+
|
|
4
|
+
Regression cover for the 2026-09-05 retry livelock residue: Qwen3.8-27B on
|
|
5
|
+
exllamav3 reasons for 4+ minutes on some tool turns. The relay collected
|
|
6
|
+
`reasoning_content` but never yielded it, so the client (Factory Droid, ~300 s
|
|
7
|
+
stream watchdog) saw `content_block_start` then total silence, disconnected,
|
|
8
|
+
and retried — each retry regenerating the turn from scratch. With the flag on,
|
|
9
|
+
leading reasoning streams as Anthropic `thinking` blocks so the client sees
|
|
10
|
+
continuous activity.
|
|
11
|
+
|
|
12
|
+
The load-bearing half is block discipline: Anthropic SSE requires blocks to
|
|
13
|
+
open/close in index order with no deltas addressed to an unopened block, in
|
|
14
|
+
every layout (thinking->text, thinking->tool, text-only, reasoning-only).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import importlib.util
|
|
19
|
+
import json
|
|
20
|
+
import unittest
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from unittest import mock
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _load_proxy_module():
|
|
26
|
+
proxy_path = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
|
|
27
|
+
spec = importlib.util.spec_from_file_location("anthropic_proxy_think", proxy_path)
|
|
28
|
+
assert spec is not None and spec.loader is not None
|
|
29
|
+
module = importlib.util.module_from_spec(spec)
|
|
30
|
+
spec.loader.exec_module(module)
|
|
31
|
+
return module
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
proxy = _load_proxy_module()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _FakeUpstreamStream:
|
|
38
|
+
"""Minimal streamed httpx.Response: yields pre-built OpenAI SSE deltas."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, deltas, finish_reason="stop"):
|
|
41
|
+
self._deltas = list(deltas)
|
|
42
|
+
self._finish_reason = finish_reason
|
|
43
|
+
self.closed = False
|
|
44
|
+
|
|
45
|
+
async def aiter_lines(self):
|
|
46
|
+
for delta in self._deltas:
|
|
47
|
+
yield "data: " + json.dumps({"choices": [{"delta": delta, "index": 0}]})
|
|
48
|
+
yield "data: " + json.dumps(
|
|
49
|
+
{"choices": [{"delta": {}, "finish_reason": self._finish_reason}]}
|
|
50
|
+
)
|
|
51
|
+
yield "data: [DONE]"
|
|
52
|
+
|
|
53
|
+
async def aclose(self):
|
|
54
|
+
self.closed = True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class _SlowUpstreamStream(_FakeUpstreamStream):
|
|
58
|
+
"""_FakeUpstreamStream with real async delays before each delta, so the
|
|
59
|
+
relay's prefill-heartbeat wait (asyncio.wait timeout) actually elapses."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, deltas, finish_reason="stop", delay=0.2):
|
|
62
|
+
super().__init__(deltas, finish_reason)
|
|
63
|
+
self._delay = delay
|
|
64
|
+
|
|
65
|
+
async def aiter_lines(self):
|
|
66
|
+
for delta in self._deltas:
|
|
67
|
+
await asyncio.sleep(self._delay)
|
|
68
|
+
yield "data: " + json.dumps({"choices": [{"delta": delta, "index": 0}]})
|
|
69
|
+
yield "data: " + json.dumps(
|
|
70
|
+
{"choices": [{"delta": {}, "finish_reason": self._finish_reason}]}
|
|
71
|
+
)
|
|
72
|
+
yield "data: [DONE]"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _drain(upstream, stream_thinking=True, heartbeat_secs=0.0):
|
|
76
|
+
monitor = proxy.SessionMonitor(context_window=131072)
|
|
77
|
+
body = {
|
|
78
|
+
"messages": [{"role": "user", "content": "go"}],
|
|
79
|
+
"tools": [
|
|
80
|
+
{"name": "Bash", "description": "run", "input_schema": {"type": "object"}}
|
|
81
|
+
],
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async def run():
|
|
85
|
+
out = []
|
|
86
|
+
with mock.patch.object(
|
|
87
|
+
proxy, "PROXY_STREAM_THINKING_DELTAS", stream_thinking
|
|
88
|
+
), mock.patch.object(
|
|
89
|
+
proxy, "PROXY_PREFILL_HEARTBEAT_SECS", heartbeat_secs
|
|
90
|
+
):
|
|
91
|
+
async for frame in proxy.stream_anthropic_response(
|
|
92
|
+
upstream, "test-model", monitor, body
|
|
93
|
+
):
|
|
94
|
+
out.append(frame)
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
return asyncio.run(run())
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _events(frames):
|
|
101
|
+
"""Parse SSE frames into a flat list of Anthropic event dicts."""
|
|
102
|
+
out = []
|
|
103
|
+
for frame in frames:
|
|
104
|
+
for line in frame.splitlines():
|
|
105
|
+
if not line.startswith("data: "):
|
|
106
|
+
continue
|
|
107
|
+
try:
|
|
108
|
+
out.append(json.loads(line[6:]))
|
|
109
|
+
except json.JSONDecodeError:
|
|
110
|
+
continue
|
|
111
|
+
return out
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _assert_block_discipline(testcase, events):
|
|
115
|
+
"""Every opened block closes exactly once, in order, and no delta or stop
|
|
116
|
+
addresses a block that is not open."""
|
|
117
|
+
expected_next = 0
|
|
118
|
+
open_index = None
|
|
119
|
+
for ev in events:
|
|
120
|
+
etype = ev.get("type")
|
|
121
|
+
if etype == "content_block_start":
|
|
122
|
+
testcase.assertIsNone(open_index, f"block {ev['index']} opened while {open_index} still open")
|
|
123
|
+
testcase.assertEqual(ev["index"], expected_next, "blocks must open in index order")
|
|
124
|
+
open_index = ev["index"]
|
|
125
|
+
expected_next += 1
|
|
126
|
+
elif etype == "content_block_delta":
|
|
127
|
+
testcase.assertIsNotNone(open_index, "delta with no open block")
|
|
128
|
+
testcase.assertEqual(ev["index"], open_index, "delta addressed to a non-open block")
|
|
129
|
+
elif etype == "content_block_stop":
|
|
130
|
+
testcase.assertEqual(ev["index"], open_index, "stop addressed to a non-open block")
|
|
131
|
+
open_index = None
|
|
132
|
+
elif etype == "message_stop":
|
|
133
|
+
testcase.assertIsNone(open_index, f"block {open_index} never closed")
|
|
134
|
+
return events
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
_DELTA_PAYLOAD_KEY = {
|
|
138
|
+
"text_delta": "text",
|
|
139
|
+
"thinking_delta": "thinking",
|
|
140
|
+
"input_json_delta": "partial_json",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _deltas_of(events, delta_type):
|
|
145
|
+
key = _DELTA_PAYLOAD_KEY[delta_type]
|
|
146
|
+
return [
|
|
147
|
+
ev["delta"][key]
|
|
148
|
+
for ev in events
|
|
149
|
+
if ev.get("type") == "content_block_delta"
|
|
150
|
+
and ev.get("delta", {}).get("type") == delta_type
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _block_kinds(events):
|
|
155
|
+
return [
|
|
156
|
+
ev["content_block"]["type"]
|
|
157
|
+
for ev in events
|
|
158
|
+
if ev.get("type") == "content_block_start"
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class TestStreamThinkingDeltas(unittest.TestCase):
|
|
163
|
+
def test_leading_reasoning_streams_as_thinking_block_then_text(self):
|
|
164
|
+
upstream = _FakeUpstreamStream(
|
|
165
|
+
[
|
|
166
|
+
{"reasoning_content": "Let me check "},
|
|
167
|
+
{"reasoning_content": "the workspace first."},
|
|
168
|
+
{"content": "Looking at the workspace."},
|
|
169
|
+
]
|
|
170
|
+
)
|
|
171
|
+
events = _assert_block_discipline(self, _events(_drain(upstream)))
|
|
172
|
+
|
|
173
|
+
self.assertEqual(_block_kinds(events), ["thinking", "text"])
|
|
174
|
+
self.assertEqual(
|
|
175
|
+
_deltas_of(events, "thinking_delta"),
|
|
176
|
+
["Let me check ", "the workspace first."],
|
|
177
|
+
)
|
|
178
|
+
self.assertEqual(
|
|
179
|
+
_deltas_of(events, "text_delta"), ["Looking at the workspace."]
|
|
180
|
+
)
|
|
181
|
+
# Thinking block must be index 0, text block index 1.
|
|
182
|
+
starts = [ev for ev in events if ev.get("type") == "content_block_start"]
|
|
183
|
+
self.assertEqual(
|
|
184
|
+
[(s["index"], s["content_block"]["type"]) for s in starts],
|
|
185
|
+
[(0, "thinking"), (1, "text")],
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def test_reasoning_then_tool_call_closes_thinking_first(self):
|
|
189
|
+
upstream = _FakeUpstreamStream(
|
|
190
|
+
[
|
|
191
|
+
{"reasoning_content": "I should run pwd."},
|
|
192
|
+
{
|
|
193
|
+
"tool_calls": [
|
|
194
|
+
{
|
|
195
|
+
"index": 0,
|
|
196
|
+
"id": "toolu_abc",
|
|
197
|
+
"function": {"name": "Bash", "arguments": '{"command":'},
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"tool_calls": [
|
|
203
|
+
{"index": 0, "function": {"arguments": ' "pwd"}'}}
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
finish_reason="tool_calls",
|
|
208
|
+
)
|
|
209
|
+
events = _assert_block_discipline(self, _events(_drain(upstream)))
|
|
210
|
+
|
|
211
|
+
self.assertEqual(_block_kinds(events), ["thinking", "tool_use"])
|
|
212
|
+
self.assertEqual(
|
|
213
|
+
_deltas_of(events, "thinking_delta"), ["I should run pwd."]
|
|
214
|
+
)
|
|
215
|
+
args = "".join(_deltas_of(events, "input_json_delta"))
|
|
216
|
+
self.assertEqual(json.loads(args), {"command": "pwd"})
|
|
217
|
+
stops = [ev for ev in events if ev.get("type") == "message_delta"]
|
|
218
|
+
self.assertEqual(stops[-1]["delta"]["stop_reason"], "tool_use")
|
|
219
|
+
|
|
220
|
+
def test_flag_off_keeps_legacy_layout_and_hides_reasoning(self):
|
|
221
|
+
upstream = _FakeUpstreamStream(
|
|
222
|
+
[
|
|
223
|
+
{"reasoning_content": "secret chain of thought"},
|
|
224
|
+
{"content": "Visible answer."},
|
|
225
|
+
]
|
|
226
|
+
)
|
|
227
|
+
events = _assert_block_discipline(
|
|
228
|
+
self, _events(_drain(upstream, stream_thinking=False))
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
self.assertEqual(_block_kinds(events), ["text"])
|
|
232
|
+
self.assertEqual(_deltas_of(events, "thinking_delta"), [])
|
|
233
|
+
self.assertEqual(_deltas_of(events, "text_delta"), ["Visible answer."])
|
|
234
|
+
starts = [ev for ev in events if ev.get("type") == "content_block_start"]
|
|
235
|
+
self.assertEqual(starts[0]["index"], 0)
|
|
236
|
+
|
|
237
|
+
def test_text_only_turn_keeps_simple_layout_under_flag(self):
|
|
238
|
+
upstream = _FakeUpstreamStream([{"content": "Just text."}])
|
|
239
|
+
events = _assert_block_discipline(self, _events(_drain(upstream)))
|
|
240
|
+
|
|
241
|
+
self.assertEqual(_block_kinds(events), ["text"])
|
|
242
|
+
self.assertEqual(_deltas_of(events, "text_delta"), ["Just text."])
|
|
243
|
+
starts = [ev for ev in events if ev.get("type") == "content_block_start"]
|
|
244
|
+
self.assertEqual(starts[0]["index"], 0)
|
|
245
|
+
|
|
246
|
+
def test_reasoning_only_turn_closes_thinking_block(self):
|
|
247
|
+
"""A turn that is all reasoning must still end well-formed: the
|
|
248
|
+
thinking block closes (and the reasoning-fallback contract decides
|
|
249
|
+
what text, if any, follows) before message_stop."""
|
|
250
|
+
upstream = _FakeUpstreamStream([{"reasoning_content": "hmm"}])
|
|
251
|
+
events = _assert_block_discipline(self, _events(_drain(upstream)))
|
|
252
|
+
|
|
253
|
+
self.assertEqual(_block_kinds(events)[0], "thinking")
|
|
254
|
+
self.assertEqual(_deltas_of(events, "thinking_delta"), ["hmm"])
|
|
255
|
+
# Well-formed stream ends with message_delta then message_stop.
|
|
256
|
+
self.assertEqual(events[-2]["type"], "message_delta")
|
|
257
|
+
self.assertEqual(events[-1]["type"], "message_stop")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class TestPrefillHeartbeat(unittest.TestCase):
|
|
261
|
+
"""PROXY_PREFILL_HEARTBEAT_SECS: one-space thinking_deltas while the
|
|
262
|
+
upstream stream is silent in the leading phase (prompt prefill).
|
|
263
|
+
|
|
264
|
+
Regression cover for the 2026-09-09 livelock: Qwen3.8-Flash-Next on a
|
|
265
|
+
single RTX 3090 prefills a 64k-token session turn in 170-400 s, past the
|
|
266
|
+
Factory Droid ~242 s stream watchdog. message_start and ping events do
|
|
267
|
+
not reset that watchdog; each kill landed mid-prefill and the retry
|
|
268
|
+
re-prefilled from token 0. The heartbeat keeps content deltas flowing
|
|
269
|
+
without changing visible output.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
def test_heartbeat_streams_thinking_spaces_before_text(self):
|
|
273
|
+
upstream = _SlowUpstreamStream([{"content": "answer"}], delay=0.22)
|
|
274
|
+
events = _assert_block_discipline(
|
|
275
|
+
self, _events(_drain(upstream, heartbeat_secs=0.05))
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
self.assertEqual(_block_kinds(events), ["thinking", "text"])
|
|
279
|
+
thinking = _deltas_of(events, "thinking_delta")
|
|
280
|
+
self.assertGreaterEqual(len(thinking), 2)
|
|
281
|
+
self.assertTrue(all(t == " " for t in thinking))
|
|
282
|
+
self.assertEqual(_deltas_of(events, "text_delta"), ["answer"])
|
|
283
|
+
|
|
284
|
+
def test_heartbeat_shares_block_with_real_reasoning(self):
|
|
285
|
+
upstream = _SlowUpstreamStream(
|
|
286
|
+
[{"reasoning_content": "real thought"}, {"content": "answer"}],
|
|
287
|
+
delay=0.12,
|
|
288
|
+
)
|
|
289
|
+
events = _assert_block_discipline(
|
|
290
|
+
self, _events(_drain(upstream, heartbeat_secs=0.05))
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
self.assertEqual(_block_kinds(events), ["thinking", "text"])
|
|
294
|
+
thinking = _deltas_of(events, "thinking_delta")
|
|
295
|
+
# Heartbeat spaces and the real reasoning share ONE thinking block.
|
|
296
|
+
self.assertEqual(thinking.count("real thought"), 1)
|
|
297
|
+
self.assertGreaterEqual(len(thinking), 3)
|
|
298
|
+
self.assertTrue(all(t in (" ", "real thought") for t in thinking))
|
|
299
|
+
|
|
300
|
+
def test_heartbeat_off_by_default_keeps_plain_text_layout(self):
|
|
301
|
+
upstream = _SlowUpstreamStream([{"content": "answer"}], delay=0.2)
|
|
302
|
+
events = _assert_block_discipline(self, _events(_drain(upstream)))
|
|
303
|
+
|
|
304
|
+
self.assertEqual(_block_kinds(events), ["text"])
|
|
305
|
+
self.assertEqual(_deltas_of(events, "thinking_delta"), [])
|
|
306
|
+
self.assertEqual(_deltas_of(events, "text_delta"), ["answer"])
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _drain_guarded(produced, delay=0.2, prefill_hb_secs=0.05):
|
|
310
|
+
"""Drive _heartbeat_then_buffered with a produce coroutine that resolves
|
|
311
|
+
after `delay` seconds; ping/timer interval shrunk to match."""
|
|
312
|
+
|
|
313
|
+
async def run():
|
|
314
|
+
async def produce():
|
|
315
|
+
await asyncio.sleep(delay)
|
|
316
|
+
return produced
|
|
317
|
+
|
|
318
|
+
out = []
|
|
319
|
+
with mock.patch.object(
|
|
320
|
+
proxy, "PROXY_STREAM_THINKING_DELTAS", True
|
|
321
|
+
), mock.patch.object(
|
|
322
|
+
proxy, "PROXY_PREFILL_HEARTBEAT_SECS", prefill_hb_secs
|
|
323
|
+
), mock.patch.object(
|
|
324
|
+
proxy, "PROXY_STREAM_HEARTBEAT_SECS", 0.05
|
|
325
|
+
):
|
|
326
|
+
async for frame in proxy._heartbeat_then_buffered(
|
|
327
|
+
produce(), "test-model", 100
|
|
328
|
+
):
|
|
329
|
+
out.append(frame)
|
|
330
|
+
return out
|
|
331
|
+
|
|
332
|
+
return asyncio.run(run())
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _text_response(text):
|
|
336
|
+
return {
|
|
337
|
+
"id": "msg_test",
|
|
338
|
+
"type": "message",
|
|
339
|
+
"role": "assistant",
|
|
340
|
+
"model": "test-model",
|
|
341
|
+
"content": [{"type": "text", "text": text}],
|
|
342
|
+
"stop_reason": "end_turn",
|
|
343
|
+
"usage": {"input_tokens": 100, "output_tokens": 5},
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class TestGuardedPathHeartbeat(unittest.TestCase):
|
|
348
|
+
"""The guarded non-stream path buffers the whole upstream generation; its
|
|
349
|
+
ping-only keep-alive did not reset the Droid ~242 s stream watchdog, so
|
|
350
|
+
long no-tool turns (e.g. session summarization, ~470 s total) livelocked
|
|
351
|
+
on 2026-09-09. With PROXY_PREFILL_HEARTBEAT_SECS on, the keep-alive is a
|
|
352
|
+
thinking block of one-space deltas and the buffered content re-indexes +1.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
def test_guarded_heartbeat_thinking_then_text_reindexed(self):
|
|
356
|
+
events = _assert_block_discipline(
|
|
357
|
+
self, _events(_drain_guarded(_text_response("summary"), delay=0.22))
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
self.assertEqual(_block_kinds(events), ["thinking", "text"])
|
|
361
|
+
thinking = _deltas_of(events, "thinking_delta")
|
|
362
|
+
self.assertGreaterEqual(len(thinking), 2)
|
|
363
|
+
self.assertTrue(all(t == " " for t in thinking))
|
|
364
|
+
self.assertEqual(_deltas_of(events, "text_delta"), ["summary"])
|
|
365
|
+
starts = [ev for ev in events if ev.get("type") == "content_block_start"]
|
|
366
|
+
self.assertEqual(
|
|
367
|
+
[(s["index"], s["content_block"]["type"]) for s in starts],
|
|
368
|
+
[(0, "thinking"), (1, "text")],
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def test_guarded_heartbeat_off_keeps_pings_and_text_at_zero(self):
|
|
372
|
+
events = _assert_block_discipline(
|
|
373
|
+
self,
|
|
374
|
+
_events(_drain_guarded(_text_response("summary"), prefill_hb_secs=0.0)),
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
self.assertEqual(_block_kinds(events), ["text"])
|
|
378
|
+
self.assertEqual(_deltas_of(events, "thinking_delta"), [])
|
|
379
|
+
starts = [ev for ev in events if ev.get("type") == "content_block_start"]
|
|
380
|
+
self.assertEqual(starts[0]["index"], 0)
|
|
381
|
+
pings = [ev for ev in events if ev.get("type") == "ping"]
|
|
382
|
+
self.assertGreaterEqual(len(pings), 2)
|
|
383
|
+
|
|
384
|
+
def test_guarded_heartbeat_error_response_closes_thinking_block(self):
|
|
385
|
+
err = proxy.Response(
|
|
386
|
+
content=json.dumps(
|
|
387
|
+
{"error": {"code": 500, "message": "boom", "type": "server_error"}}
|
|
388
|
+
),
|
|
389
|
+
status_code=500,
|
|
390
|
+
media_type="application/json",
|
|
391
|
+
)
|
|
392
|
+
frames = _drain_guarded(err, delay=0.22)
|
|
393
|
+
events = _assert_block_discipline(self, _events(frames))
|
|
394
|
+
|
|
395
|
+
self.assertEqual(_block_kinds(events), ["thinking"])
|
|
396
|
+
# Thinking block closed before the terminal SSE error frame.
|
|
397
|
+
stop_at = next(
|
|
398
|
+
i for i, f in enumerate(frames) if "content_block_stop" in f
|
|
399
|
+
)
|
|
400
|
+
error_at = next(i for i, f in enumerate(frames) if "event: error" in f)
|
|
401
|
+
self.assertLess(stop_at, error_at)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
if __name__ == "__main__":
|
|
405
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/dist/cli/rtk.d.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* RTK (Rust Token Killer) Integration for UAP
|
|
4
|
-
*
|
|
5
|
-
* Provides commands to install and manage RTK CLI proxy
|
|
6
|
-
* for 60-90% token savings on command outputs.
|
|
7
|
-
*/
|
|
8
|
-
export interface RTKInstallOptions {
|
|
9
|
-
force?: boolean;
|
|
10
|
-
method?: 'homebrew' | 'cargo' | 'curl';
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Install RTK with auto-detection of best method
|
|
14
|
-
*/
|
|
15
|
-
export declare function installRTK(options?: RTKInstallOptions): Promise<void>;
|
|
16
|
-
/**
|
|
17
|
-
* Check RTK status
|
|
18
|
-
*/
|
|
19
|
-
export declare function checkRTKStatus(): Promise<void>;
|
|
20
|
-
/**
|
|
21
|
-
* Run an arbitrary RTK command by passing through all args to the rtk binary.
|
|
22
|
-
* If RTK is not installed, prints install instructions.
|
|
23
|
-
*/
|
|
24
|
-
export declare function runRTKCommand(args: string[]): void;
|
|
25
|
-
/**
|
|
26
|
-
* Show unified analytics combining MCP Router stats and RTK stats.
|
|
27
|
-
* Reads MCP Router config from .uap-mcp-router.json and RTK stats from `rtk gain`.
|
|
28
|
-
*/
|
|
29
|
-
export declare function showUnifiedAnalytics(): void;
|
|
30
|
-
/**
|
|
31
|
-
* Show RTK help
|
|
32
|
-
*/
|
|
33
|
-
export declare function showRTKHelp(): void;
|
|
34
|
-
//# sourceMappingURL=rtk.d.ts.map
|
package/dist/cli/rtk.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"rtk.d.ts","sourceRoot":"","sources":["../../src/cli/rtk.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AAMH,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;CACxC;AA+ID;;GAEG;AACH,wBAAsB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyF/E;AAED;;GAEG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CA4CpD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAkBlD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CA+D3C;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,IAAI,CAqClC"}
|